﻿/*
   Copyright 2012 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.ServiceModel;
using System.ServiceProcess;
using System.Linq;
using Flower.Logging;
using System.Threading.Tasks;
using Flower.Services.Data;
using System.Collections.Generic;
using Flower.Security;
using Flower.Directory.Util;
using Spring.Aop.Framework;
using Spring.Aop.Support;
using System.Collections.Concurrent;
using System.Runtime.Remoting.Lifetime;
using System.Collections;

namespace Flower.Services.Host
{
    public partial class Service : ServiceBase
    {
        private class ServiceHostInfo
        {
            public IServiceHostManager Manager;
            public AppDomain AppDomain;
        }

        private static readonly Log Log = LogManager.CreateLog(typeof(Service).Name);

        private IDirectory _directory;
        public IDirectory Directory { set { _directory = value; } }
        
        private IStateService _stateService;
        public IStateService StateService { set { _stateService = value; } }

        private IQueueService _queueService;
        public IQueueService QueueService { set { _queueService = value; } }

        private ServiceHost _directoryServiceHost;
        private ServiceHost _stateServiceHost;
        private ServiceHost _queueServiceHost;

        private readonly ClientSponsor _sponsor = new ClientSponsor();

        private IEnumerable<string> _allowSaveStateToUsers;
        private IEnumerable<string> _allowSaveStateToRoles;
        private IEnumerable<string> _allowEnqueueToUsers;
        private IEnumerable<string> _allowEnqueueToRoles;
        private IEnumerable<string> _allowDequeueToUsers;
        private IEnumerable<string> _allowDequeueToRoles;
        private IEnumerable<string> _allowQueueManagementToUsers;
        private IEnumerable<string> _allowQueueManagementToRoles;

        private static IEnumerable<string> SplitSemicolonSeparatedList(string list)
        {
            return list.Split(';').Select(item => item.Trim()).Where(item => item.Length > 0);
        }

        public string AllowSaveStateToUsers { set { _allowSaveStateToUsers = SplitSemicolonSeparatedList(value); } }
        public string AllowSaveStateToRoles { set { _allowSaveStateToRoles = SplitSemicolonSeparatedList(value); } }
        public string AllowEnqueueToUsers { set { _allowEnqueueToUsers = SplitSemicolonSeparatedList(value); } }
        public string AllowEnqueueToRoles { set { _allowEnqueueToRoles = SplitSemicolonSeparatedList(value); } }
        public string AllowDequeueToUsers { set { _allowDequeueToUsers = SplitSemicolonSeparatedList(value); } }
        public string AllowDequeueToRoles { set { _allowDequeueToRoles = SplitSemicolonSeparatedList(value); } }
        public string AllowQueueManagementToUsers { set { _allowQueueManagementToUsers = SplitSemicolonSeparatedList(value); } }
        public string AllowQueueManagementToRoles { set { _allowQueueManagementToRoles = SplitSemicolonSeparatedList(value); } }

        private string[] _hostedServices;
        public IList HostedServices
        {
            set { _hostedServices = value.Cast<string>().ToArray(); }
        }

        private ServiceHostInfo[] _serviceHosts;

        public Service()
        {
            InitializeComponent();
        }

        #region StartStop

        protected override void OnStart(string[] args)
        {
            DoStart();
        }

        protected override void OnStop()
        {
            DoStop();
        }

        internal void DoStart()
        {
            if (_directory != null)
            {
                Log.Trace("Starting the directory service.");

                try
                {
                    _directoryServiceHost = new ServiceHost(new Directory(_directory));
                    _directoryServiceHost.Open();
                }
                catch(Exception ex)
                {
                    Log.Fatal("Failed to start the directory service.", ex);
                }

                Task.Factory.StartNew(NotifyProcessorsOnStartup);
            }

            if (_stateService != null)
            {
                Log.Trace("Starting the state service.");

                try
                {
                    _stateServiceHost = new ServiceHost(MakeStateService());
                    _stateServiceHost.Open();
                }
                catch (Exception ex)
                {
                    Log.Fatal("Failed to start the state service.", ex);
                }
            }

            if (_queueService != null)
            {
                Log.Trace("Starting the queue service.");

                try
                {
                    _queueServiceHost = new ServiceHost(MakeQueueService());
                    _queueServiceHost.Open();
                }
                catch (Exception ex)
                {
                    Log.Fatal("Failed to start the queue service.", ex);
                }
            }

            if (_hostedServices != null)
            {
                Log.Trace("Starting service hosts.");

                var serviceHosts = new ConcurrentBag<ServiceHostInfo>();

                _hostedServices
                    .AsParallel()
                    .ForAll(path => StartServiceHost(path, serviceHosts));
                
                _serviceHosts = serviceHosts.ToArray();
            }

            Log.Info("Service started.");
        }

        private void StartServiceHost(string configurationPath, IProducerConsumerCollection<ServiceHostInfo> serviceHosts)
        {
            try
            {
                Log.Trace(() => string.Format("Starting service host {0}.", configurationPath));

                AppDomain currentDomain = AppDomain.CurrentDomain;
                AppDomain appDomain =
                    AppDomain.CreateDomain
                    (
                        configurationPath,
                        null,
                        currentDomain.BaseDirectory,
                        currentDomain.RelativeSearchPath,
                        false
                    );

                var manager =
                    (IServiceHostManager)appDomain.CreateInstanceAndUnwrap
                    (
                        System.Reflection.Assembly.GetExecutingAssembly().FullName,
                        typeof(ServiceHostManager).FullName ?? typeof(ServiceHostManager).Name
                    );
                _sponsor.Register((MarshalByRefObject)manager);

                appDomain.UnhandledException += OnUnhandledServiceException;

                if (manager.Start(configurationPath))
                {
                    serviceHosts.TryAdd
                    (
                        new ServiceHostInfo
                        {
                            Manager = manager,
                            AppDomain = appDomain
                        }
                    );
                }
                else
                {
                    AppDomain.Unload(appDomain);
                }
            }
            catch (Exception ex)
            {
                Log.Error(string.Format("Failed to start service host {0}.", configurationPath), ex);
            }
        }

        private static void OnUnhandledServiceException(object sender, UnhandledExceptionEventArgs e)
        {
            Log.Error(string.Format("Unhandled exception in the service host.\n{0}", e.ExceptionObject));
        }

        internal void DoStop()
        {
            if (_queueService != null)
            {
                Log.Trace("Stopping the queue service.");

                _queueServiceHost.Close();

                var disposableQueueService = _queueService as IDisposable;

                if (disposableQueueService != null)
                {
                    disposableQueueService.Dispose();
                }
            }

            if (_stateService != null)
            {
                Log.Trace("Stopping the state service.");

                _stateServiceHost.Close();

                var disposableStateService = _stateService as IDisposable;

                if (disposableStateService != null)
                {
                    disposableStateService.Dispose();
                }
            }

            if (_directory != null)
            {
                Log.Trace("Stopping the directory service.");

                _directoryServiceHost.Close();

                var disposableDirectory = _directory as IDisposable;

                if (disposableDirectory != null)
                {
                    disposableDirectory.Dispose();
                }
            }

            if (_serviceHosts != null)
            {
                _serviceHosts
                    .AsParallel()
                    .ForAll(StopServiceHost);
            }

            Log.Info("Service stopped.");
        }

        private void StopServiceHost(ServiceHostInfo serviceHost)
        {
            string serviceHostName = serviceHost.AppDomain.FriendlyName;

            try
            {
                Log.Trace(() => string.Format("Stopping service host {0}.", serviceHostName));

                serviceHost.Manager.Stop();
                AppDomain.Unload(serviceHost.AppDomain);
            }
            catch (Exception ex)
            {
                Log.Error(string.Format("Failed to stop service host {0}.", serviceHostName), ex);
            }
        }

        #endregion

        #region AOP

        private IQueueService MakeQueueService()
        {
            var proxyFactory = new ProxyFactory(_queueService);

            proxyFactory.AddAdvisor
            (
                new DefaultPointcutAdvisor
                {
                    Advice = new DefaultLogging("QueueService"),
                    Order = 10
                }
            );

            proxyFactory.AddAdvisor
            (
                new DefaultPointcutAdvisor
                {
                    Pointcut = new NameMatchMethodPointcut { MappedName = "Enqueue" },
                    Advice = new AuthorizationAdvice(_allowEnqueueToUsers, _allowEnqueueToRoles),
                    Order = 20
                }
            );

            proxyFactory.AddAdvisor
            (
                new DefaultPointcutAdvisor
                {
                    Pointcut = new NameMatchMethodPointcut { MappedName = "Dequeue" },
                    Advice = new AuthorizationAdvice(_allowDequeueToUsers, _allowDequeueToRoles),
                    Order = 20
                }
            );

            var queueManagementMethods =
                typeof(IQueueService)
                    .GetMethods()
                    .Select(meth => meth.Name)
                    .Where(name => name != "Enqueue" && name != "Dequeue");

            foreach (string queueManagementMethod in queueManagementMethods)
            {
                proxyFactory.AddAdvisor
                (
                    new DefaultPointcutAdvisor
                    {
                        Pointcut = new NameMatchMethodPointcut { MappedName = queueManagementMethod },
                        Advice = new AuthorizationAdvice(_allowQueueManagementToUsers, _allowQueueManagementToRoles),
                        Order = 20
                    }
                );
            }

            return new QueueService((IQueueService)proxyFactory.GetProxy());
        }

        private IStateService MakeStateService()
        {
            var proxyFactory = new ProxyFactory(_stateService);

            proxyFactory.AddAdvisor
            (
                new DefaultPointcutAdvisor
                {
                    Advice = new DefaultLogging("StateService"),
                    Order = 10
                }
            );

            proxyFactory.AddAdvisor
            (
                new DefaultPointcutAdvisor
                {
                    Advice = new AuthorizationAdvice(_allowSaveStateToUsers, _allowSaveStateToRoles),
                    Order = 20
                }
            );

            return new StateService((IStateService)proxyFactory.GetProxy());
        }

        #endregion

        #region NotifyProcessorsOnStartup

        private void NotifyProcessorsOnStartup()
        {
            Log.Trace("Notifying processors on the directory startup.");

            IList<DirectoryEntry> processorEntries;

            using (new AuthorizationOverride("Administrator"))
            {
                processorEntries =
                    _directory
                        .GetChildren
                        (
                            "/Processors",
                            DirectoryEntryTypes.Processor,
                            SortProperty.Default,
                            SortOrder.Asc,
                            0,
                            long.MaxValue
                        );
            }

            processorEntries
                .OfType<Processor>()
                .AsParallel()
                .ForAll
                (
                    processor =>
                    {
                        IProcessor procService;

                        try
                        {
                            procService = DirectoryUtil.GetService<IProcessor>(processor, "processor");
                        }
                        catch (Exception ex)
                        {
                            Log.Trace(() => string.Format("Failed to obtain service interface of the processor {0}.\n{1}", processor.Name, ex));
                            return;
                        }

                        if (procService != null)
                        {
                            try
                            {
                                Log.Trace(() => string.Format("Notifying {0} on startup.", processor.Name));
                                procService.CheckPendingProcesses();
                            }
                            catch (Exception ex)
                            {
                                Log.Trace(() => string.Format("Failed to notify the processor {0} on startup.\n{1}", processor.Name, ex));
                            }
                        }
                    }
                );
        }

        #endregion
    }
}
